package bancosys.tec.persist.persister;

import static jmine.tec.persist.PersistMessages.AUTHORIZATION_ERROR;
import static jmine.tec.persist.PersistMessages.STALE_STATE_IN_AUTHORIZATION;
import static jmine.tec.persist.PersistMessages.VALIDATION_ERROR;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.security.auth.Subject;

import jmine.tec.persist.PersistMessages;

import org.hibernate.Criteria;
import org.hibernate.EntityMode;
import org.hibernate.Hibernate;
import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.StaleObjectStateException;
import org.hibernate.TransientObjectException;
import org.hibernate.criterion.Restrictions;
import org.hibernate.engine.CascadeStyle;
import org.hibernate.engine.CascadingAction;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.type.Type;
import org.hibernate.validator.InvalidStateException;
import org.hibernate.validator.InvalidValue;

import bancosys.tec.exception.BusinessException;
import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.authorization.Authorizable;
import bancosys.tec.persist.authorization.AuthorizableActionAware;
import bancosys.tec.persist.authorization.AuthorizationContext;
import bancosys.tec.persist.authorization.AuthorizationMode;
import bancosys.tec.persist.authorization.AuthorizationStatus;
import bancosys.tec.persist.authorization.Authorizer;
import bancosys.tec.persist.authorization.DataAlreadyInAuthorizationException;
import bancosys.tec.persist.authorization.annotation.DisplayName;
import bancosys.tec.persist.controller.PersistenceEnabledController;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.persist.hibernate.PartitionSessionFactory;
import bancosys.tec.persist.persister.listener.AuthorizablePersisterListener;
import bancosys.tec.persist.persister.listener.PersisterListener;
import bancosys.tec.persist.validator.ValidationError;
import bancosys.tec.persist.validator.ValidationException;
import bancosys.tec.persist.validator.Validator;
import bancosys.tec.utils.date.Timestamp;

/**
 * @created Feb 13, 2007
 * @author Gustavo Almeida
 * @param <T> classe do dado persistível.
 */
public class DefaultPersister<T extends Persistable> extends AbstractPersister<T> implements Authorizer<T> {

    private boolean authorizable;

    private boolean authorizationEnabled;

    private ClassMetadata cm;

    /**
     * Define o bean que será tratado por esse persister, descobre se ele é autorizável e descobre o nome da sua partição fria.
     * 
     * @param target bean.
     */
    @Override
    public final void setTarget(T target) {
        super.setTarget(target);

        // descobre se o bo é autorizável.
        SessionFactory sessionFactory = this.getSessionFactory();
        if (sessionFactory instanceof PartitionSessionFactory) {
            PartitionSessionFactory partitionSessionFactory = (PartitionSessionFactory) sessionFactory;
            this.authorizable = partitionSessionFactory.getBoAuthorizationManager().isAuthorizable(target);
            this.authorizationEnabled = partitionSessionFactory.getBoAuthorizationManager().isAuthorizationEnabled();
        } else {
            this.authorizable = false;
        }
    }

    /**
     * Devolve <code>true</code> se o bean sendo tratado deve ser autorizado, <code>false</code> caso contrário.
     * 
     * @return <code>true</code> se o bean sendo tratado deve ser autorizado, <code>false</code> caso contrário.
     */
    public final boolean isAuthorizable() {
        return this.authorizable;
    }

    /**
     * Retorna true caso seja possível/permitido autorizar ou rejeitar o alvo em autorização.
     * 
     * @param target entidade em autorização
     * @param action action
     * @return boolean
     */
    private boolean mayAuthorize(Authorizable target, Object action) {
        AuthorizationMode mode = target.getAuthorizationMode(action);
        return mode == null || !mode.isForce() || AuthorizationStatus.DISABLED.equals(mode.getStatus());
    }

    /**
     * Determina no caso de ações de save e delete se a entidade deve ser enviada para a base fria ou não.
     * 
     * @param action action
     * @return true caso deva mandar para base fria, false caso contrário
     */
    private boolean shouldSendToCold(Object action) {
        if (!this.authorizable) {
            return false;
        }
        Authorizable target = (Authorizable) this.getTarget();
        AuthorizationMode mode = target.getAuthorizationMode(action);
        if (mode != null && mode.isForce()) {
            return AuthorizationStatus.ENABLED.equals(mode.getStatus());
        } else {
            return this.authorizationEnabled;
        }
    }

    // =========================

    /**
     * Executa o processamento necessário para trazer um dado da base fria para a base quente.
     * 
     * @param original dado frio.
     * @return dado pronto para se colocado na base quente.
     */
    @SuppressWarnings("unchecked")
    private T fromColdToHot(T original) {
        T copy = (T) this.copyObject(original);
        this.fromColdToHotRecursion((Authorizable) copy, new Timestamp());

        Session session = this.getSession();
        session.delete(original);
        session.flush();

        return copy;
    }

    /**
     * Recursão do <code>fromColdToHot</code>.
     * 
     * @param target dado frio.
     * @param timestamp hora.
     */
    private void fromColdToHotRecursion(Authorizable target, Timestamp timestamp) {
        target.setAuth(Boolean.FALSE);
        target.setPk(target.getAuthId());
        target.setAuthId(null);
        target.setPersistenceAction(null);
        target.setTimeStamp(timestamp);

        for (Object element : this.getCascadeValues(target)) {
            if (element instanceof Authorizable) {
                this.fromColdToHotRecursion((Authorizable) element, timestamp);
            } else {
                // Objeto não-autorizável (element) encontrado em relacionamento
                // com cascade de objeto autorizável (target)
                throw new PersistenceException(PersistMessages.NOT_AUTHORIZABLE_OBJECT_CASCADED_FROM_AUTHORIZABLE.create(element, target));
            }

        }
    }

    /**
     * Valida se uma determinada operação pode ser incluída na base fria, garantindo que existirá apenas uma operação a ser autorizada por
     * bo.
     * 
     * @param copy dado frio.
     */
    private void validateColdOperation(Authorizable copy) {
        if (copy.getAuthId() == null) {
            return;
        }
        PartitionSessionFactory sf = (PartitionSessionFactory) this.getSessionFactory();
        Session session = this.getSession();
        AuthorizationContext context = sf.changeSessionAuthorizationContext(session, AuthorizationContext.cold);
        try {
            Criteria crit = session.createCriteria(copy.getClass()).add(Restrictions.eq("authId", copy.getAuthId()));
            int count = crit.list().size();
            if (count == 0) {
                return;
            }
            if (count == 1) {
                throw new DataAlreadyInAuthorizationException(copy.getClass(), copy.getAuthId());
            }
            if (count > 1) {
                throw new PersistenceException(PersistMessages.MORE_THAN_ONE_REGISTER_WAITING_AUTHORIZATION.create(copy.getClass(),
                        copy.getAuthId()));
            }
        } finally {
            sf.changeSessionAuthorizationContext(session, context);
        }
    }

    /**
     * Coloca um dado quente na base fria.
     * 
     * @param original dado quente.
     * @param action action
     */
    private void fromHotToCold(Authorizable original, Object action) {
        if (this.getSessionFactory() instanceof PartitionSessionFactory) {
            Authorizable copy = (Authorizable) this.copyObject(original);
            Session session = this.getSession();
            this.fromHotToColdRecursion(copy, original.getPersistenceAction(), new Timestamp());
            this.setSaveAction(copy, action);
            this.validateColdOperation(copy);
            session.saveOrUpdate(copy);
            session.flush();
            this.fireAfterInsertColdOperation(copy.getPersistenceAction(), copy);
            session.evict(copy);
            Serializable id = this.getTargetClassMetadata().getIdentifier(copy, EntityMode.POJO);
            session.load(original, id);
        }
    }

    /**
     * Chama os callbacks de {@link AuthorizablePersisterListener}
     * 
     * @param persistenceAction o {@link PersistenceAction}
     * @param copy {@link Authorizable}
     */
    private void fireAfterInsertColdOperation(PersistenceAction persistenceAction, Authorizable copy) {
        for (PersisterListener<T> persisterListener : this.getListeners()) {
            if (persisterListener instanceof AuthorizablePersisterListener<?>) {
                @SuppressWarnings("unchecked")
                AuthorizablePersisterListener<Authorizable> listener = (AuthorizablePersisterListener<Authorizable>) persisterListener;
                listener.afterInsertIntoColdPartition(persistenceAction, copy);
            }
        }
    }

    /**
     * Recursão do <code>fromHotToCold</code>.
     * 
     * @param target dado quente.
     * @param timestamp hora.
     * @param persistenceAction ação de persistência.
     */
    private void fromHotToColdRecursion(Authorizable target, PersistenceAction persistenceAction, Timestamp timestamp) {
        String username = this.getCurrentUser(target.getController());
        target.setTimeStamp(timestamp);
        target.setAuthId(target.getPk());
        target.setUser(username);
        target.setPk(null);
        target.setAuth(Boolean.TRUE);
        target.setPersistenceAction(persistenceAction);

        for (Object element : this.getCascadeValues(target)) {
            if (element instanceof Authorizable) {
                this.fromHotToColdRecursion((Authorizable) element, persistenceAction, new Timestamp());
            } else {
                // Objeto não-autorizável (element) encontrado em relacionamento
                // com cascade de objeto autorizável (target)
                throw new PersistenceException(PersistMessages.NOT_AUTHORIZABLE_OBJECT_CASCADED_FROM_AUTHORIZABLE.create(element, target));
            }
        }
    }

    /**
     * Devolve o nome do usuário atual da thread.
     * 
     * @param controller controller.
     * @return nome do usuário atual.
     */
    private String getCurrentUser(PersistenceEnabledController controller) {
        Subject subject = controller.getSecurityService().getCurrentThreadSubject();
        String username = "";
        if (subject != null) {
            Principal principal = subject.getPrincipals().iterator().next();
            username = principal.getName();
        }
        return username;
    }

    /**
     * Copia recursivamente um objeto respeitando os 'cascades' e 'lazy's do hibernate
     * 
     * @param original objeto original.
     * @return cópia do objeto original.
     */
    private Object copyObject(Object original) {
        return this.copyObjectRecursionRecursion(original, new IdentityHashMap<Object, Persistable>());
    }

    /**
     * Recursão do método <code>copyObject</code>.
     * 
     * @param original objeto original
     * @param objetosCopiados objetos já copiados.
     * @return objeto copiado.
     */
    @SuppressWarnings("unchecked")
    private Object copyObjectRecursionRecursion(Object original, IdentityHashMap objetosCopiados) {
        if (original == null) {
            return null;
        }

        if (!(original instanceof Persistable)) {
            return original;
        }

        BaseDAO dao = this.getTarget().getController().getDAOFactory().getGenericDAO(this.getRealClass(original.getClass()));
        ClassMetadata classMetadata = this.getSessionFactory().getClassMetadata(this.getRealClass(original.getClass()));
        Persistable associatedPersistable = null;

        if (this.getSession().contains(original)) {
            associatedPersistable = (Persistable) original;
        } else {
            Serializable pk = classMetadata.getIdentifier(original, EntityMode.POJO);
            if (pk != null) {
                try {
                    associatedPersistable = dao.findByPk(pk);
                } catch (BeanNotFoundException e) {
                    // Erro carregando dados originais de objeto da classe
                    // "this.getRealClass(original.getClass()).getName()" com pk
                    // "pk"
                    throw new PersistenceException(PersistMessages.ERROR_LOADING_ORIGINAL_DATA.create(this
                            .getRealClass(original.getClass()).getName(), pk), e);
                }
            } else {
                associatedPersistable = (Persistable) original;
            }
        }

        // se já passamos por esse cara alguma vez
        if (objetosCopiados.containsKey(original)) {
            return objetosCopiados.get(original);
        }

        Persistable copy = dao.createBean();
        objetosCopiados.put(original, copy);
        objetosCopiados.put(associatedPersistable, copy);

        classMetadata.setIdentifier(copy, classMetadata.getIdentifier(original, EntityMode.POJO), EntityMode.POJO);
        String[] properties = classMetadata.getPropertyNames();

        for (int i = 0; i < properties.length; i++) {
            String property = properties[i];
            Type type = classMetadata.getPropertyType(property);
            Object value = classMetadata.getPropertyValue(original, property, EntityMode.POJO);
            if (objetosCopiados.containsKey(value)) {
                value = objetosCopiados.get(value);
            }

            // Não é um tipo persistível, portanto a referência pode ser para o
            // mesmo objeto
            if (!type.isAssociationType() || value == null) {
                classMetadata.setPropertyValue(copy, property, value, EntityMode.POJO);
            } else {
                if (!Hibernate.isInitialized(value)) {
                    value = classMetadata.getPropertyValue(associatedPersistable, property, EntityMode.POJO);
                }
                if (!isCascadeSave(((AbstractEntityPersister) classMetadata).getCascadeStyle(i))) {
                    if (type.isCollectionType()) {
                        this.copyShallowCollection(objetosCopiados, classMetadata, copy, property, value);
                    } else {
                        classMetadata.setPropertyValue(copy, property, value, EntityMode.POJO);
                    }
                } else {
                    if (type.isCollectionType()) {
                        this.deepCopyCollection(objetosCopiados, classMetadata, copy, property, value);
                    } else {
                        classMetadata.setPropertyValue(copy, property, this.copyObjectRecursionRecursion(value, objetosCopiados),
                                EntityMode.POJO);
                    }
                }
            }
        }

        try {
            this.getSession().evict(original);
        } catch (Throwable e) {
            // empty;
        }

        try {
            this.getSession().evict(associatedPersistable);
        } catch (TransientObjectException e) {
            // empty;
        }

        return copy;
    }

    /**
     * Faz uma copia total do valor passado, que deve ser uma colecao mapeada no hibernate.
     * 
     * @param objetosCopiados os objetos ja copiados
     * @param classMetadata o {@link ClassMetadata} do dono da colecao
     * @param copy o dono da colecao
     * @param property o nome da propriedade
     * @param value o valor
     */
    @SuppressWarnings("unchecked")
    private void deepCopyCollection(IdentityHashMap objetosCopiados, ClassMetadata classMetadata, Persistable copy, String property,
            Object value) {
        final Object collectionCopy;
        if (value.getClass().isArray()) {
            int length = Array.getLength(value);
            collectionCopy = Array.newInstance(value.getClass().getComponentType(), length);
            for (int i = 0; i < length; i++) {
                Object toCopy = Array.get(value, i);
                Object newValue = this.copyObjectRecursionRecursion(toCopy, objetosCopiados);
                Array.set(collectionCopy, i, newValue);
            }
        } else if (value instanceof Map) {
            collectionCopy = this.deepCopyMap((Map<Object, Object>) value, objetosCopiados);
        } else {
            collectionCopy = this.deepCopyListOrSet((Collection) value, objetosCopiados);
        }
        classMetadata.setPropertyValue(copy, property, collectionCopy, EntityMode.POJO);
    }

    /**
     * Copia o objeto passado como uma colecao - ou seja, seu {@link Type} no hibernate deve ser uma colecao.
     * 
     * @param objetosCopiados os objetos copiados
     * @param classMetadata o {@link ClassMetadata} da copia
     * @param copy a copia sendo construida
     * @param property o nome da propriedade na copia
     * @param value o valor a ser copiado
     */
    @SuppressWarnings("unchecked")
    private void copyShallowCollection(IdentityHashMap objetosCopiados, ClassMetadata classMetadata, Persistable copy, String property,
            Object value) {
        final Object shallowCopy;
        if (value.getClass().isArray()) {
            int length = Array.getLength(value);
            shallowCopy = Array.newInstance(value.getClass().getComponentType(), length);
            for (int i = 0; i < length; i++) {
                Object toCopy = Array.get(value, i);
                Object newValue = this.getOrDefault(objetosCopiados, toCopy);
                Array.set(shallowCopy, i, newValue);
            }
        } else if (value instanceof Map) {
            shallowCopy = this.shallowCopyMap((Map<Object, Object>) value, objetosCopiados);
        } else {
            shallowCopy = this.shallowCopyListOrSet((Collection) value, objetosCopiados);
        }
        classMetadata.setPropertyValue(copy, property, shallowCopy, EntityMode.POJO);
    }

    /**
     * Usado na recursão do método <code>copyObject</code> para executar "deep copy" de coleções - ou seja, derivadas de {@link Collection}.
     * 
     * @param c coleção
     * @param objetosCopiados objetos copiados
     * @return cópia da coleção
     */
    @SuppressWarnings("unchecked")
    private Collection deepCopyListOrSet(Collection c, IdentityHashMap objetosCopiados) {
        Collection newCollection;
        if (c instanceof List) {
            newCollection = new ArrayList(c.size());
        } else if (c instanceof SortedSet) {
            newCollection = new TreeSet(((SortedSet) c).comparator());
        } else if (c instanceof Set) {
            newCollection = new HashSet();
        } else {
            throw new PersistenceException(PersistMessages.UNSUPORTED_COLLECTION_TYPE.create(c.getClass().getName()));
        }

        for (Object obj : c) {
            newCollection.add(this.copyObjectRecursionRecursion(obj, objetosCopiados));
        }

        return newCollection;
    }

    /**
     * Usa o metodo {@link #copyObjectRecursionRecursion(Object, IdentityHashMap)} para copiar as entradas no mapa. Mapas devem ser mapeados
     * de acordo com os exemplos de PaiComMapa
     * 
     * @param map o {@link Map}
     * @param objetosCopiados o {@link IdentityHashMap} com os objetos copiados
     * @return {@link Map}
     */
    private Map<Object, Object> deepCopyMap(Map<Object, Object> map, IdentityHashMap<Object, Object> objetosCopiados) {
        Map<Object, Object> copy = new HashMap<Object, Object>(map.size());
        Set<Entry<Object, Object>> entrySet = map.entrySet();
        for (Entry<Object, Object> entry : entrySet) {
            final Object newKey = this.copyObjectRecursionRecursion(entry.getKey(), objetosCopiados);
            final Object newValue = this.copyObjectRecursionRecursion(entry.getValue(), objetosCopiados);
            copy.put(newKey, newValue);
        }
        return copy;
    }

    /**
     * Usado na recursão do método <code>copyObject</code> para executar "shallow copy" de coleções.
     * 
     * @param c coleção
     * @param objetosCopiados objetos copiados
     * @return cópia da coleção
     */
    @SuppressWarnings("unchecked")
    private Collection shallowCopyListOrSet(Collection c, IdentityHashMap objetosCopiados) {
        Collection newCollection;

        if (c instanceof List) {
            newCollection = new ArrayList(c.size());
        } else if (c instanceof SortedSet) {
            newCollection = new TreeSet(((SortedSet) c).comparator());
        } else if (c instanceof Set) {
            newCollection = new HashSet();
        } else {
            throw new PersistenceException(PersistMessages.UNSUPORTED_COLLECTION_TYPE.create(c.getClass().getName()));
        }

        for (Object obj : c) {
            newCollection.add(this.getOrDefault(objetosCopiados, obj));
        }
        return newCollection;
    }

    /**
     * Faz uma copia shallow (ou seja, copia as referencias ao inves de copiar os objetos referenciados) no mapa. Mapas devem ser mapeados
     * de acordo com os exemplos de PaiComMapa
     * 
     * @param map o {@link Map} a ser copiado
     * @param copied um {@link IdentityHashMap}
     * @return {@link Map} a copia
     */
    private Map<Object, Object> shallowCopyMap(Map<Object, Object> map, IdentityHashMap<Object, Object> copied) {
        Map<Object, Object> copy = new HashMap<Object, Object>(map.size());
        Set<Entry<Object, Object>> entrySet = map.entrySet();
        for (Entry<Object, Object> entry : entrySet) {
            final Object newKey = this.getOrDefault(copied, entry.getKey());
            final Object newValue = this.getOrDefault(copied, entry.getValue());
            copy.put(newKey, newValue);
        }
        return copy;
    }

    /**
     * Devolve o valor presente no {@link IdentityHashMap} ou o valor passado
     * 
     * @param copied {@link IdentityHashMap}
     * @param defaultEntry {@link Object}
     * @return {@link Object}
     */
    private Object getOrDefault(IdentityHashMap<Object, Object> copied, Object defaultEntry) {
        Object newKey;
        if (copied.containsKey(defaultEntry)) {
            newKey = copied.get(defaultEntry);
        } else {
            newKey = defaultEntry;
        }
        return newKey;
    }

    /**
     * Devolve os valores das propriedades cascade de um objeto.
     * 
     * @param object objeto.
     * @return os valores das propriedades cascade de um objeto.
     */
    @SuppressWarnings("unchecked")
    private Collection getCascadeValues(Object object) {
        Collection list = new LinkedList();
        ClassMetadata classMetadata = this.getSessionFactory().getClassMetadata(this.getRealClass(object.getClass()));
        Type[] types = classMetadata.getPropertyTypes();
        Object[] values = classMetadata.getPropertyValues(object, EntityMode.POJO);

        for (int i = 0; i < types.length; i++) {
            Type type = types[i];
            if (!type.isAssociationType()) {
                continue;
            }

            CascadeStyle cascadeStyle = ((AbstractEntityPersister) classMetadata).getCascadeStyle(i);
            if (!isCascadeSave(cascadeStyle)) {
                continue;
            }

            if (type.isCollectionType()) {
                if (Map.class.isAssignableFrom(type.getReturnedClass())) {
                    // Mapas devem ser mapeados de acordo com os exemplos de
                    // {@link PaiComMapa}
                    this.addAllNotNull((Map) values[i], list);
                } else {
                    this.addAllNotNull((Collection) values[i], list);
                }
            } else {
                if (values[i] != null) {
                    list.add(values[i]);
                }
            }
        }
        return list;
    }

    /**
     * Adiciona todos os elementos diferentes de <code>null</code> de <code>source</code> ao <code>target</code>.
     * 
     * @param source origem.
     * @param target destino.
     */
    @SuppressWarnings("unchecked")
    private void addAllNotNull(Collection source, Collection target) {
        if (source == null) {
            return;
        }

        for (Object element : source) {
            if (element != null) {
                target.add(element);
            }
        }
    }

    /**
     * Adiciona todos os elementos diferentes de <code>null</code> de <code>source</code> ao <code>target</code>. Este método assume que as
     * chaves (caso sejam entidades) já estão persistidas. O mapa não poderá ser persistido caso contrário.
     * 
     * @param source origem.
     * @param target destino.
     */
    @SuppressWarnings("unchecked")
    private void addAllNotNull(Map source, Collection target) {
        if (source == null) {
            return;
        }

        for (Object element : source.values()) {
            if (element != null) {
                target.add(element);
            }
        }
    }

    // =======================================

    /**
     * Autoriza uma operação.
     */
    @SuppressWarnings("deprecation")
    public final void authorize() {
        if (!this.isAuthorizable()) {
            return;
        }
        Authorizable coldInstance = (Authorizable) this.getTarget();
        try {
            getSession().lock(coldInstance, LockMode.UPGRADE);
        } catch(StaleObjectStateException e) {
            throw new BusinessException(STALE_STATE_IN_AUTHORIZATION.create(this.getTargetNarrowedName(), coldInstance.getPk()), e);
        }
        Serializable id = null;
        if (coldInstance.getAuthId() == null) {
            id = coldInstance.getPk();
        } else {
            id = coldInstance.getAuthId();
        }

        if (!Boolean.TRUE.equals(coldInstance.getAuth())) {
            throw new BusinessException(PersistMessages.DATA_ALREADY_AUTHORIZED.create(this.getTargetNarrowedName(), id));
        }

        Object action = this.getSaveAction(coldInstance);
        this.setSaveAction(coldInstance, null);

        if (!this.mayAuthorize(coldInstance, action)) {
            throw new BusinessException(PersistMessages.AUTHORIZATION_NOT_PERMITTED.create(this.getTargetNarrowedName(), id));
        }

        try {
            Validator<T> validator = this.getValidator();
            List<ValidationError> validationErrors = null;
            PersistenceAction persistenceAction = coldInstance.getPersistenceAction();

            T hotInstance = this.fromColdToHot(this.getTarget());

            this.fireBeforeAuthorization(coldInstance.getPersistenceAction(), hotInstance);

            SessionFactory sessionFactory = this.getSessionFactory();
            if (sessionFactory instanceof PartitionSessionFactory) {
                PartitionSessionFactory partitionSessionFactory = (PartitionSessionFactory) sessionFactory;
                if (!partitionSessionFactory.getBoAuthorizationManager().isAuthorizableBySameUser()) {
                    String currentUser = this.getCurrentUser(coldInstance.getController());
                    if (currentUser.equalsIgnoreCase(coldInstance.getUser())) {
                        throw new BusinessException(PersistMessages.SAME_USER_AUTHORIZATION.create(this.getTargetNarrowedName(), id));
                    }
                }
            }

            if (persistenceAction.equals(PersistenceAction.remove)) {
                this.removeDependencies(hotInstance);
                validationErrors = validator.validateRemove(hotInstance);
                if (validationErrors != null && validationErrors.size() > 0) {
                    throw new ValidationException(VALIDATION_ERROR.create(this.getTargetNarrowedName(), id), validationErrors);
                }
                this.beforeRemove(hotInstance);
                this.fireBeforeRemove(hotInstance);
                this.audit(hotInstance, persistenceAction, id);
                this.doRemove(hotInstance);
                this.afterRemove(hotInstance);
                this.fireAfterRemove(hotInstance);
            } else if (persistenceAction.equals(PersistenceAction.insert) || persistenceAction.equals(PersistenceAction.insertWAction)) {
                validationErrors = validator.validateInsert(hotInstance);
                if (validationErrors != null && validationErrors.size() > 0) {
                    throw new ValidationException(VALIDATION_ERROR.create(this.getTargetNarrowedName(), id), validationErrors);
                }
                this.beforeInsert(hotInstance);
                this.fireBeforeInsert(hotInstance, action);
                this.doInsertOrUpdate(hotInstance, null);

                // A partir desse ponto sempre usar o target pois ele sempre
                // possui o id
                this.audit(this.getTarget(), persistenceAction, this.getTarget().getPk());
                this.afterInsert(this.getTarget());
                this.fireAfterInsert(this.getTarget(), action);
            } else if (persistenceAction.equals(PersistenceAction.update) || persistenceAction.equals(PersistenceAction.updateWAction)) {
                validationErrors = validator.validateUpdate(hotInstance);
                if (validationErrors != null && validationErrors.size() > 0) {
                    throw new ValidationException(VALIDATION_ERROR.create(this.getTargetNarrowedName(), id), validationErrors);
                }
                this.beforeUpdate(hotInstance);
                this.fireBeforeUpdate(hotInstance, action);

                this.audit(hotInstance, persistenceAction, id);
                this.doInsertOrUpdate(hotInstance, null);

                // Depois do update o target que deve ser utilizado.
                // O objeto hotInstance nao esta sincronizado com o banco, SE
                // ele possuir uma colecao (mesmo com cascade) e na atualizacao foi incluido um
                // novo objeto nessa colecao, o hotInstance possui ele esse novo objeto como
                // TRANSIENT,quando o Target (sincronizado com o banco) possui o novo
                // objeto PERSISTIDO.
                this.afterUpdate(this.getTarget());
                this.fireAfterUpdate(this.getTarget(), action);
            } else {
                throw new BusinessException(PersistMessages.AUTHORIZATION_DENIED.create(persistenceAction, this.getTargetNarrowedName(), id));
            }
            this.getSession().flush();
        } catch (PersistenceException e) {
            coldInstance.setAuth(Boolean.TRUE);
            throw new BusinessException(AUTHORIZATION_ERROR.create(this.getTargetNarrowedName(), id), e);
        } catch(ValidationException e) {
            coldInstance.setAuth(Boolean.TRUE);
            throw e;
        } catch (BusinessException e) {
            coldInstance.setAuth(Boolean.TRUE);
            throw e;
        } catch (StaleObjectStateException e) {
            coldInstance.setAuth(Boolean.TRUE);
            throw new BusinessException(STALE_STATE_IN_AUTHORIZATION.create(this.getTargetNarrowedName(), id), e);
        } catch (Throwable e) {
            coldInstance.setAuth(Boolean.TRUE);
            throw new PersistenceException(AUTHORIZATION_ERROR.create(this.getTargetNarrowedName(), id), e);
        }
    }

    /**
     * Obtem o melhor nome para classe, ou seja, Display Name quando a anotação existir e Class.getName caso contrário;
     * 
     * @param clazz a classe
     * @return String nome;
     */
    @SuppressWarnings("unchecked")
    private String narrowNameForClass(Class clazz) {
        Class realClass = this.getRealClass(clazz);
        
        if (realClass.getAnnotation(DisplayName.class) != null) {
            return ((DisplayName) realClass.getAnnotation(DisplayName.class)).value();
        }
        return realClass.getName();
    }
    
    /**
     * Obtem o melhor nome para classe do target;
     * 
     * @return o display name ou o da classe
     */
    private String getTargetNarrowedName() {
        return this.narrowNameForClass(this.getTarget().getClass());
    }

    /**
     * @param instance entidade
     * @return save action da entidade.
     */
    private Object getSaveAction(Persistable instance) {
        if (instance instanceof AuthorizableActionAware) {
            return ((AuthorizableActionAware) instance).getAuthorizationAction();
        } else {
            return null;
        }
    }

    /**
     * @param instance entidade
     * @param action save action da entidade.
     */
    private void setSaveAction(Persistable instance, Object action) {
        if (instance instanceof AuthorizableActionAware) {
            ((AuthorizableActionAware) instance).setAuthorizationAction(action);
        }
    }

    /**
     * @param persistenceAction {@link PersistenceAction}
     * @param hotInstance the hot instance
     */
    private void fireBeforeAuthorization(PersistenceAction persistenceAction, T hotInstance) {
        for (PersisterListener<T> persisterListener : this.getListeners()) {
            if (persisterListener instanceof AuthorizablePersisterListener<?>) {
                @SuppressWarnings("unchecked")
                AuthorizablePersisterListener<Authorizable> listener = (AuthorizablePersisterListener<Authorizable>) persisterListener;
                listener.beforeAuthorization(persistenceAction, (Authorizable) hotInstance);
            }
        }
    }

    /**
     * @param persistenceAction {@link PersistenceAction}
     * @param hotInstance T
     */
    private void fireBeforeRejection(PersistenceAction persistenceAction, T hotInstance) {
        for (PersisterListener<T> persisterListener : this.getListeners()) {
            if (persisterListener instanceof AuthorizablePersisterListener<?>) {
                @SuppressWarnings("unchecked")
                AuthorizablePersisterListener<Authorizable> listener = (AuthorizablePersisterListener<Authorizable>) persisterListener;
                listener.beforeRejection(persistenceAction, (Authorizable) hotInstance);
            }
        }
    }

    /**
     * Rejeita uma operação.
     */
    @SuppressWarnings("unchecked")
    public final void reject() {
        if (this.isAuthorizable()) {
            Authorizable target = (Authorizable) this.getTarget();

            Serializable id = null;
            if (target.getAuthId() == null) {
                id = target.getPk();
            } else {
                id = target.getAuthId();
            }

            if (!Boolean.TRUE.equals(target.getAuth())) {
                throw new BusinessException(PersistMessages.DATA_NOT_IN_AUTHORIZATION.create(this.getTargetNarrowedName(), id));
            }
            if (!this.mayAuthorize(target, this.getSaveAction(target))) {
                throw new BusinessException(PersistMessages.REJECTION_NOT_PERMITTED.create(this.getTargetNarrowedName(), id));
            }
            try {
                Session session = this.getSession();
                this.fireBeforeRejection(target.getPersistenceAction(), (T) target);

                this.audit(target, PersistenceAction.reject, id);

                session.delete(target);
                session.flush();
            } catch (PersistenceException e) {
                throw e;
            } catch (BusinessException e) {
                throw e;
            } catch (Throwable e) {
                throw new PersistenceException(PersistMessages.ERROR_REJECTING_AUTHORIZATION.create(this.getTargetNarrowedName(), id), e);
            }
        }
    }

    /**
     * Remove um bean, se ele for autorizável ele não será removido, mas enviado à autorização.
     */
    @SuppressWarnings("deprecation")
    public final void remove() {
        Validator<T> validator = this.getValidator();
        List<ValidationError> validationErrors = null;

        try {
            if (this.shouldSendToCold(null)) {
                Authorizable target = (Authorizable) this.getTarget();
                final Serializable id;
                if (target.getAuthId() == null) {
                    id = target.getPk();
                } else {
                    id = target.getAuthId();
                }
                if (((Authorizable) this.getTarget()).getAuth()) {
                    if (!this.mayAuthorize((Authorizable) this.getTarget(), this.getSaveAction(this.getTarget()))) {
                        throw new PersistenceException(PersistMessages.REJECTION_NOT_PERMITTED.create(this.getTarget()));
                    }
                    this.audit(this.getTarget(), PersistenceAction.coldRemove, id);
                    Session session = this.getSession();
                    Object obj = session.merge(this.getTarget());
                    session.delete(obj);
                } else {
                    target.setPersistenceAction(PersistenceAction.remove);

                    this.changePartition(AuthorizationContext.hot);
                    validationErrors = this.sumErrors(validationErrors, validator.validateRemoveAuthorization(this.getTarget()));
                    if (validationErrors != null && validationErrors.size() > 0) {
                        throw new ValidationException(validationErrors);
                    }

                    this.fromHotToCold(target, null);
                    this.audit(target, PersistenceAction.coldRemove, id);
                }
                this.getSession().flush();
            } else {
                T target = this.getTarget();
                this.removeDependencies(target);
                validationErrors = validator.validateRemove(target);
                if (validationErrors != null && validationErrors.size() > 0) {
                    throw new ValidationException(validationErrors);
                }
                this.beforeRemove(target);
                this.fireBeforeRemove(target);
                this.audit(target, PersistenceAction.remove, target.getPk());
                this.getSession().delete(target);
                this.afterRemove(target);
                this.fireAfterRemove(target);
            }
        } catch (PersistenceException e) {
            throw e;
        } catch (BusinessException e) {
            throw e;
        } catch (Throwable e) {
            throw new PersistenceException(PersistMessages.ERROR_REMOVING_BEAN.create(), e);
        } finally {
            this.changePartition(AuthorizationContext.hot);
        }
    }

    /**
     * Além de salvar a entidade, como em {@link DefaultPersister#save()}, se a entidade for do tipo {@link AuthorizableActionAware}, salva
     * a ação que causou o bean ir para autorização.
     * 
     * @param action ação que está enviando a entidade para autorização.
     */
    @SuppressWarnings("deprecation")
    public final void save(Object action) {
        Validator<T> validator = this.getValidator();
        List<ValidationError> validationErrors = null;

        try {
            if (this.shouldSendToCold(action)) {
                Authorizable target = (Authorizable) this.getTarget();
                if (this.inAuthorization()) { // Editando um dado frio.
                    this.doInsertOrUpdate(this.getTarget(), action);
                    this.fireAfterUpdateColdInstance(this.getTarget());
                    this.audit(this.getTarget(), PersistenceAction.coldUpdate,
                            (target.getAuthId() != null ? target.getAuthId() : target.getPk()));
                } else { // editando dado quente (enviando para a fria)

                    PersistenceAction persistenceAction = null;
                    if (PersistenceAction.update.equals(this.insertOrUpdate())) {
                        target.setPersistenceAction(action == null ? PersistenceAction.update : PersistenceAction.updateWAction);
                        this.changePartition(AuthorizationContext.hot);
                        validationErrors = this.sumErrors(validationErrors, validator.validateUpdateAuthorization(this.getTarget()));
                        persistenceAction = PersistenceAction.coldUpdate;
                    } else {
                        target.setPersistenceAction(action == null ? PersistenceAction.insert : PersistenceAction.insertWAction);
                        this.changePartition(AuthorizationContext.hot);
                        validationErrors = this.sumErrors(validationErrors, validator.validateInsertAuthorization(this.getTarget()));
                        persistenceAction = PersistenceAction.coldInsert;
                    }
                    if (validationErrors != null && validationErrors.size() > 0) {
                        throw new ValidationException(validationErrors);
                    }
                    this.fromHotToCold(target, action);
                    this.audit(target, persistenceAction, target.getAuthId());
                }
                this.getSession().flush();
            } else {
                T target = this.getTarget();
                if (PersistenceAction.update.equals(this.insertOrUpdate())) {
                    validationErrors = validator.validateUpdate(target);
                    if (validationErrors != null && validationErrors.size() > 0) {
                        throw new ValidationException(validationErrors);
                    }

                    this.beforeUpdate(target);
                    this.fireBeforeUpdate(target, action);

                    this.getSession().saveOrUpdate(target);

                    this.audit(target, PersistenceAction.update, target.getPk());

                    this.afterUpdate(target);
                    this.fireAfterUpdate(target, action);
                } else {
                    validationErrors = validator.validateInsert(target);
                    if (validationErrors != null && validationErrors.size() > 0) {
                        throw new ValidationException(validationErrors);
                    }
                    this.beforeInsert(target);
                    this.fireBeforeInsert(target, action);

                    this.getSession().saveOrUpdate(target);

                    this.audit(target, PersistenceAction.insert, target.getPk());

                    this.afterInsert(target);
                    this.fireAfterInsert(target, action);
                }
            }
        } catch (PersistenceException e) {
            throw e;
        } catch (BusinessException e) {
            throw e;
        } catch (InvalidStateException e) {
            if (validationErrors == null) {
                validationErrors = new ArrayList<ValidationError>();
            }
            validationErrors.add(this.mountValidationError(e.getInvalidValues()));
            throw new ValidationException(validationErrors);
        } catch (Throwable e) {
            // neste caso repassa como uma persistenceexception cuja mensagem é a mensagem da causa
            throw new PersistenceException(PersistMessages.UNEXPECTED_ERROR.create(e.getMessage()), e);
        } finally {
            this.changePartition(AuthorizationContext.hot);
        }
    }

    /**
     * Salva (save or update) um bean, se ele for autorizável ele não será salvo, mas enviado à autorização.
     */
    public final void save() {
        this.save(null);
    }

    /**
     * @param target the target
     */
    private void fireAfterUpdateColdInstance(T target) {
        for (PersisterListener<T> persisterListener : this.getListeners()) {
            if (persisterListener instanceof AuthorizablePersisterListener<?>) {
                @SuppressWarnings("unchecked")
                AuthorizablePersisterListener<Authorizable> listener = (AuthorizablePersisterListener<Authorizable>) persisterListener;
                listener.afterUpdateColdInstance((Authorizable) target);
            }
        }
    }

    /**
     * Monta um {@link ValidationError} a partir de um array de {@link InvalidValue}, causado pelas exceções de validação do Hibernate.
     * 
     * @param invalidValues array contendo os erros de validação do Hibernate.
     * @return {@link ValidationError} contendo uma mensagem de erro com todos os erros de validação que aconteceram.
     */
    private ValidationError mountValidationError(InvalidValue[] invalidValues) {
        StringBuffer errorMsg = new StringBuffer();
        if (invalidValues != null) {
            for (InvalidValue iv : invalidValues) {
                errorMsg.append(iv.getPropertyName());
                errorMsg.append("(" + iv.getValue() + ")");
                errorMsg.append(":");
                errorMsg.append(iv.getMessage());
                errorMsg.append(";");
            }
            errorMsg = errorMsg.replace(errorMsg.length() - 1, errorMsg.length(), ".");
        }
        return new ValidationError(PersistMessages.VALIDATION_ERROR_HIBERNATE_VALIDATION_2.create(this.getTarget(), errorMsg));
    }

    /**
     * Se o target está em autorização (na base fria).
     * 
     * @return <code>true</code> se o target estiver na base fria.
     */
    private boolean inAuthorization() {
        return Boolean.TRUE.equals(((Authorizable) this.getTarget()).getAuth());
    }

    /**
     * Troca a partição corrente para a partição indicada.
     * 
     * @param context <code>AuthorizationContext.hot</code> ou <code>AuthorizationContext.cold</code>.
     * @return a partição anterior.
     */
    protected final AuthorizationContext changePartition(AuthorizationContext context) {
        SessionFactory factory = this.getSessionFactory();
        if (factory instanceof PartitionSessionFactory) {
            PartitionSessionFactory sessionFactory = (PartitionSessionFactory) this.getSessionFactory();
            return sessionFactory.changeSessionAuthorizationContext(this.getSession(), context);
        }
        return context;
    }

    /**
     * Dadas duas listas, se uma delas for <code>null</code>, devolve a outra, se ambas forem <code>null</code> devolve <code>null</code>,
     * se ambas forem diferentes de <code>null</code> devolve uma terceira lista com o conteudo das duas.
     * 
     * @param errors lista 1
     * @param moreErrors lista 2
     * @return soma das duas listas ou <code>null</code>.
     */
    protected final List<ValidationError> sumErrors(List<ValidationError> errors, List<ValidationError> moreErrors) {
        if (errors == null && moreErrors == null) {
            return null;
        }
        if (errors != null && moreErrors == null) {
            return errors;
        }
        if (errors == null && moreErrors != null) {
            return moreErrors;
        }
        List<ValidationError> totalErrors = new ArrayList<ValidationError>(errors);
        totalErrors.addAll(moreErrors);
        return totalErrors;
    }

    /**
     * Devolve o <code>ClassMetadata</code> da classe <code>target</code>.
     * 
     * @return o <code>ClassMetadata</code> da classe <code>target</code>.
     */
    private ClassMetadata getTargetClassMetadata() {
        if (this.cm == null) {
            this.cm = this.getSessionFactory().getClassMetadata(this.getTarget().getClass());
        }
        return this.cm;
    }

    // =============================================

    /**
     * Persiste um dado na base.
     * 
     * @param bean dado a ser persistido.
     * @param action action
     */
    private void doInsertOrUpdate(T bean, Object action) {
        Session session = this.getSession();
        this.setSaveAction(bean, action);
        Object obj = session.merge(bean);
        session.flush();
        session.evict(obj);
        session.evict(bean);
        Serializable id = this.getTargetClassMetadata().getIdentifier(obj, EntityMode.POJO);
        session.load(this.getTarget(), id);
        session.flush();
    }

    /**
     * Executa a remoção de dependencias na base fria e na base quente.
     * 
     * @param bean bean;
     * @see #doRemoveDependencies(Persistable)
     */
    @SuppressWarnings("deprecation")
    protected void removeDependencies(T bean) {
        if (this.getSessionFactory() instanceof PartitionSessionFactory) {
            PartitionSessionFactory sf = (PartitionSessionFactory) this.getSessionFactory();
            Session session = this.getSession();
            // frio
            AuthorizationContext originalContext = sf.changeSessionAuthorizationContext(session, AuthorizationContext.cold);
            this.doRemoveDependencies(bean);
            this.fireRemoveDependencies(bean);
            // quente
            sf.changeSessionAuthorizationContext(session, AuthorizationContext.hot);
            this.doRemoveDependencies(bean);
            this.fireRemoveDependencies(bean);
            // volta p/ contexto original
            sf.changeSessionAuthorizationContext(session, originalContext);
        } else {
            this.doRemoveDependencies(bean);
            this.fireRemoveDependencies(bean);
        }
    }

    /**
     * Executa qquer processo que precise ser executado durante a remoção mas antes da validação.
     * 
     * @param bean bean.
     * @deprecated ESSE METODO SERA REMOVIDO ATE A VERSAO 1.0.65 DO JMINE-TEC. Se for desejavel adicionar comportamento quando ocorrer esse
     * evento, deve ser utilizado a infra-estrutura de PersisterListener ao invez de estender o DefaultPersister.
     */
    @Deprecated
    protected void doRemoveDependencies(T bean) {
        // nothing
    }

    /**
     * Executa a remoção bean na base quente.
     * 
     * @param bean bean.
     */
    private void doRemove(T bean) {
        Session session = this.getSession();
        Object obj = session.merge(bean);
        session.delete(obj);
        session.flush();
    }

    /**
     * Executado antes do insert na base quente.
     * 
     * @param bean bean.
     * @deprecated ESSE METODO SERA REMOVIDO ATE A VERSAO 1.0.65 DO JMINE-TEC. Se for desejavel adicionar comportamento quando ocorrer esse
     * evento, deve ser utilizado a infra-estrutura de PersisterListener ao invez de estender o DefaultPersister.
     */
    @Deprecated
    protected void afterInsert(T bean) {
        // pode ser implementado quem extender a classe.
    }

    /**
     * Executado após o insert na base quente.
     * 
     * @param bean bean.
     * @deprecated ESSE METODO SERA REMOVIDO ATE A VERSAO 1.0.65 DO JMINE-TEC. Se for desejavel adicionar comportamento quando ocorrer esse
     * evento, deve ser utilizado a infra-estrutura de PersisterListener ao invez de estender o DefaultPersister.
     */
    @Deprecated
    protected void beforeInsert(T bean) {
        // pode ser implementado quem extender a classe.
    }

    /**
     * Executado antes do update na base quente.
     * 
     * @param bean bean.
     * @deprecated ESSE METODO SERA REMOVIDO ATE A VERSAO 1.0.65 DO JMINE-TEC. Se for desejavel adicionar comportamento quando ocorrer esse
     * evento, deve ser utilizado a infra-estrutura de PersisterListener ao invez de estender o DefaultPersister.
     */
    @Deprecated
    protected void afterUpdate(T bean) {
        // pode ser implementado quem extender a classe.
    }

    /**
     * Executado após o update na base quente.
     * 
     * @param bean bean.
     * @deprecated ESSE METODO SERA REMOVIDO ATE A VERSAO 1.0.65 DO JMINE-TEC. Se for desejavel adicionar comportamento quando ocorrer esse
     * evento, deve ser utilizado a infra-estrutura de PersisterListener ao invez de estender o DefaultPersister.
     */
    @Deprecated
    protected void beforeUpdate(T bean) {
        // pode ser implementado quem extender a classe.
    }

    /**
     * Executado antes do remove na base quente.
     * 
     * @param bean bean.
     * @deprecated ESSE METODO SERA REMOVIDO ATE A VERSAO 1.0.65 DO JMINE-TEC. Se for desejavel adicionar comportamento quando ocorrer esse
     * evento, deve ser utilizado a infra-estrutura de PersisterListener ao invez de estender o DefaultPersister.
     */
    @Deprecated
    protected void afterRemove(T bean) {
        // pode ser implementado quem extender a classe.
    }

    /**
     * Executado após o remove na base quente.
     * 
     * @param bean bean.
     * @deprecated ESSE METODO SERA REMOVIDO ATE A VERSAO 1.0.65 DO JMINE-TEC. Se for desejavel adicionar comportamento quando ocorrer esse
     * evento, deve ser utilizado a infra-estrutura de PersisterListener ao invez de estender o DefaultPersister.
     */
    @Deprecated
    protected void beforeRemove(T bean) {
        // pode ser implementado quem extender a classe.
    }

    /**
     * Dada uma classe decorada por CGLIB devolve a classe original, se a classe não for decorada por CGLIB devolve a própria classe.
     * 
     * @param clazz classe decorada.
     * @return classe base.
     */
    private Class getRealClass(Class clazz) {
        if (clazz.getName().indexOf("$$") > 0) {
            return clazz.getSuperclass();
        } else {
            return clazz;
        }
    }

    /**
     * Verifica se o relacionamento passado deve ser 'cascateado'
     * 
     * @param style {@link CascadeStyle}
     * @return boolean
     */
    public static boolean isCascadeSave(CascadeStyle style) {
        return style.doCascade(CascadingAction.SAVE_UPDATE);
    }
}
